package com.pinfly.common.http;

import java.io.FileInputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.security.cert.X509Certificate;

import org.apache.log4j.Logger;

import com.pinfly.common.util.EnvUtil;
import com.pinfly.common.util.StringUtil;

/**
 * This class contains several utility routines used by HttpsServer and
 * HttpsClient.
 */
class SSLUtil
{
    private static final Logger logger = Logger.getLogger (SSLUtil.class);

    // Some property names used by the various classes
    public static final String CERT_ALIAS = "CertAlias";
    public static final String CLIENT_AUTH = "ClientAuth";
    public static final String KEY_STORE_FILE = "KeyStoreFile";
    public static final String PASS_PHRASE = "PassPhrase";
    public static final String PEER_ID = "PeerId";

    // Values for parameters that aren't configurable
    private static final String SSL_CONTEXT_TYPE = "TLS";
    private static final String KEYSTORE_TYPE = "JKS";
    private static final String KEY_MANAGER_TYPE = "SunX509";

    // We cache SSL Context's based on the certificate used.
    // That cache is kept here
    private static final HashMap s_cache = new HashMap ();

    // A default SocketFactory for our s_provider, which uses the
    // default keymanager and trust manager.
    private static SSLSocketFactory s_defaultSocketFactory = null;

    /**
     * Gets an SSLSocketFactory which uses the default keymanager and trust
     * manager, and our specified provider
     * 
     * @return The socket factory
     * @throws NoSuchAlgorithmException If the SSL algorithmn can not be found
     * @throws KeyManagementException if there are any problems with the default
     *             key store.
     */
    static synchronized SSLSocketFactory getDefaultSSLSocketFactory () throws NoSuchAlgorithmException,
                                                                      KeyManagementException
    {
        if (s_defaultSocketFactory == null)
        {
            // Get the proper SSLContext for our specific provider
            SSLContext context = SSLContext.getInstance (SSL_CONTEXT_TYPE);

            // Use the default key manager and trust manager
            context.init (null, null, null);

            s_defaultSocketFactory = context.getSocketFactory ();
        }

        return s_defaultSocketFactory;
    }

    /**
     * Get an SSL Context and initialize it to a particular certificate.
     * 
     * @param keystoreFile Name of the keystore file.
     * @param passPhrase Pass phrase for retrieving the key from the keystore
     *            file.
     * @param certAlias The alias of the certificate/private key pair in the
     *            keystore file.
     * @returns An SSL Context initialized with the specified private key and
     *          certificate
     */
    static synchronized SSLContext getSSLContext (String keystoreFile, String passPhrase, String certAlias)
                                                                                                           throws Exception
    {
        // Look in the cache first
        ContextKey key = new ContextKey (keystoreFile, certAlias);
        SSLContext context = (SSLContext) s_cache.get (key);
        if (context != null)
        {
            // Was in the cache
            return context;
        }

        // Not in the cache - have to make one

        // Get a keystore and load it from the file
        KeyStore ks = KeyStore.getInstance (KEYSTORE_TYPE);
        ks.load (new FileInputStream (keystoreFile), passPhrase.toCharArray ());

        // Remove all the keys from the keystore except the one we want
        logger.trace ("Using alias " + certAlias + " from keystore " + keystoreFile);
        if (!ks.containsAlias (certAlias))
        {
            Exception e = new Exception ("Keystore file (" + keystoreFile + ") does not contain alias " + certAlias);

            logger.fatal (e.getMessage (), e);
            throw e;
        }

        for (Enumeration aliases = ks.aliases (); aliases.hasMoreElements ();)
        {
            String aka = (String) aliases.nextElement ();
            if (!aka.equalsIgnoreCase (certAlias))
            {
                ks.deleteEntry (aka);
            }
        }

        // Get a key manager factory and initialize it from the keystore
        KeyManagerFactory kmf = KeyManagerFactory.getInstance (KEY_MANAGER_TYPE);
        kmf.init (ks, passPhrase.toCharArray ());

        // Finally, create an SSL Context and initialize it with the key manager
        context = SSLContext.getInstance (SSL_CONTEXT_TYPE);
        context.init (kmf.getKeyManagers (), null, null);

        // Put it in the cache in case its used again later
        s_cache.put (key, context);

        return context;
    }

    /**
     * Validate a certificate. Checks the subject name field in the certificate
     * against a list of valid ids. If the name contains any of the valid ids,
     * the routine returns. If the name does not contain at least one of the
     * valid ids then an MSHttpException is thrown.
     * 
     * @param cert The certificate to validate
     * @param validIds The list of valid ids
     * @throws MSHttpInvalidCertificateException if the certificate subject name
     *             doesn't contain any of the valid ids.
     */
    static void validateCert (X509Certificate cert, String[] validIds) throws MSHttpInvalidCertificateException
    {
        if (cert == null)
        {
            throw new MSHttpInvalidCertificateException ("Certificate to evaluate was null");
        }
        if (validIds == null || validIds.length == 0)
        {
            throw new MSHttpInvalidCertificateException ("No ids to validate against");
        }

        String name = cert.getSubjectDN ().getName ();
        List ids = getNameParts (name);

        for (Iterator it = ids.iterator (); it.hasNext ();)
        {
            String id = (String) it.next ();
            for (int i = 0; i < validIds.length; i++)
            {
                String vid = validIds[i];
                if (vid == null || vid.length () == 0)
                    continue;
                if (vid.equalsIgnoreCase (id))
                {
                    // A valid id
                    return;
                }
            }
        }

        // If get here, nothing matched
        throw new MSHttpInvalidCertificateException ("Certificate name (" + name + ") is not valid");
    }

    /**
     * Parse an X500 Distinguished Name. Takes an X500 DN and returns a list of
     * strings, each of which contains an element of the name. For example, the
     * DN <code>ou=CCOW.ContextManager,o=Pinfly,L=Mesa</code> would return a
     * list containing the following three elements:
     * <code>ou=CCOW.ContextManager<br>
     * 	o=Pinfly<br>
     * 	L=Mesa</code></br> The elements are formatted to remove spaces around
     * the = sign. Any spaces embedded in the values are left unchanged.
     * 
     * @param dn The name to parse
     * @returns The elements of the parsed name
     */
    private static List getNameParts (String dn)
    {
        List parts = new ArrayList ();

        char[] c = dn.toCharArray ();
        int length = c.length;
        int pos = 0;
        StringBuffer part = new StringBuffer ();

        // This code assumes that the DN is well formed. Since the
        // DN is coming from a certificate, that seems like a reasonable
        // assumption. If the DN is not well formed then this routine may
        // throw exceptions - especially an array bounds exception.
        do
        {
            part.delete (0, part.length ());

            // pos is the character after the , that delimited the previous
            // part.
            // Skip any whitespace
            while (Character.isWhitespace (c[pos]))
                pos++;

            // Find the = that delimits the part type
            while (c[pos] != '=')
                part.append (c[pos++]);
            // Get the =
            part.append (c[pos++]);

            // Is the value quoted?
            boolean quoted = c[pos] == '"';
            if (quoted)
                pos++;

            // Find the end of the value
            // If the value is quoted, the end is the next non-escaped quote
            // If the value is not quoted, the end is the next non-escaped ,
            // or the end of the string.
            for (; pos < length; pos++)
            {
                if (c[pos] == '\\')
                {
                    // An escaped character - ignore the \ and take
                    // the next character
                    pos++;
                }
                else if (quoted && c[pos] == '"')
                {
                    // That closes the quote, and we don't want
                    // to include the quoted character
                    quoted = false;
                    continue;
                }
                else if (!quoted && c[pos] == ',')
                {
                    // End of value
                    pos++;
                    break;
                }
                // Just another character
                part.append (c[pos]);
            }
            parts.add (part.toString ());
        }
        while (pos < length);

        return parts;
    }

    /**
     * Make sure an id is in the proper format. The id should be
     * <code>name=value<code>.
     * 	This routine verifies that is the case and also strips out any
     * 	spaces around the = sign.  If the id is valid, it returns the
     * 	(possibly modified) id.  If the id is not valid, it throws an
     * 	IllegalArgumentException.
     */
    static String validateId (String id)
    {
        if (id != null)
        {
            StringTokenizer tok = new StringTokenizer (id, "=");
            if (tok.countTokens () == 2)
            {
                String nm = tok.nextToken ().trim ();
                String val = tok.nextToken ().trim ();
                if (nm.length () > 0 && val.length () > 0)
                {
                    // All okay
                    return nm + '=' + val;
                }
            }
        }

        // If get here, there was a problem
        throw new IllegalArgumentException ("Invalid id specification");
    }

    /**
     * Get a property from a property file. The property value is searched for
     * in steps. First a value is searched for using
     * <code>appName.propName</code>. If that value is found, it is used. If
     * not, then a value is searched for using <code>*.propName</code>. If that
     * value is found, it is used. If not the default value is used.
     * 
     * @param props The properties to search.
     * @param propName The name of the property to look for
     * @param appName The application containing the property
     * @param def The default value
     * @return The property value.
     */
    static String getProp (Properties props, String propName, String appName, String def)
    {
        String value = props.getProperty (appName + '.' + propName);
        if (value == null)
        {
            value = props.getProperty ("*." + propName, def);
        }

        if (KEY_STORE_FILE.equals (propName))
        {
            // Leaving for backwards compatibility incase some other code
            // is using %ENV%
            value = StringUtil.expandEnvVar (value);
            value = EnvUtil.resolveVariables (value);
        }
        return value;
    }

    /** Class that is used as the key to the SSLContext cache */
    private static class ContextKey
    {
        private final String m_keystore;
        private final String m_alias;
        private final int m_hashCode;

        private ContextKey (String keyStore, String alias)
        {
            m_keystore = keyStore;
            m_alias = alias;
            m_hashCode = keyStore.hashCode () * 7 + alias.hashCode () * 13;
        }

        public int hashCode ()
        {
            return m_hashCode;
        }

        public boolean equals (Object o)
        {
            if (o instanceof ContextKey)
            {
                ContextKey other = (ContextKey) o;
                return m_hashCode == other.m_hashCode && m_keystore.equals (other.m_keystore)
                       && m_alias.equals (other.m_alias);
            }
            else
            {
                return false;
            }
        }

        public String toString ()
        {
            return "ContextKey - " + m_keystore + ':' + m_alias;
        }
    }

    // private static void main (String[] args)
    // {
    // testGetNameParts
    // ("CN=www.pinfly.com, OU=CCOW.ContextManager, O=Pinfly Corporation, L=Mesa, ST=Arizona, C=US");
    // testGetNameParts
    // ("CN=www.pinfly.com, OU=CCOW.ContextManager, O=Pinfly\\, Incorporated, L=Mesa, ST=Arizona, C=US");
    // testGetNameParts
    // ("CN=www.pinfly.com, OU=CCOW.ContextManager, O=\"Pinfly, Incorporated\", L=Mesa, ST=Arizona, C=US");
    // testValidateId ("ou=ccow.contextManager");
    // testValidateId ("ou =ccow.contextManager");
    // testValidateId ("ou=   ccow.contextManager");
    // testValidateId ("ou = ccow.contextManager");
    // testValidateId ("");
    // testValidateId ("     ");
    // testValidateId ("ou ccow.contextManager");
    // testValidateId ("ou=");
    // testValidateId ("=ccow.cm");
    // testValidateId ("   =ccow.cm");
    // testValidateId ("ou=      ");
    // testValidateId ("   =        ");
    // }
    // private static void testGetNameParts (String dn)
    // {
    // List l = getNameParts (dn);
    // System.out.println ("Parts of '" + dn + "' are:");
    // for (Iterator it = l.iterator(); it.hasNext(); )
    // {
    // System.out.println ((String)it.next());
    // }
    // System.out.println ("");
    // }
    // private static void testValidateId (String id)
    // {
    // try
    // {
    // String id2 = validateId (id);
    // System.out.println ("id '" + id + "' is valid" +
    // (!id.equals(id2) ? " converted to '" + id2 + "'" : ""));
    // }
    // catch (IllegalArgumentException e)
    // {
    // System.out.println ("id '" + id + "' is NOT valid");
    // }
    // }

}
